﻿using System;
using System.Collections.Generic;
using System.Threading;
using System.Data.SQLite;
using UlsStudio.Engine.Events;
using UlsStudio.Engine.Parsers;
using UlsStudio.Engine.SqlParsers;
using UlsStudio.Engine.Database;

namespace UlsStudio.Engine
{
    /// <summary>
    /// A set of ULS log entries. Can populate self from existing ULS files or from real-time ULS data.
    /// </summary>
    public class UlsDataSet : IDisposable
    {
        #region Constructors, Destructors & Privates

        private readonly SQLiteConnection _databaseConnection = null;
        private readonly SQLiteCommand _insertCommand = null;
        private FileSetJobLoadingStatus _status = null;
        private LogFileWatcher _logfileWatcher = null;

        public UlsDataSet()
        {
            // Setup DB
            if (!DbHelper.DbCreated)
            {
                DbHelper.CreateTempDatabase();
            }
            // Vars
            _databaseConnection = DbHelper.GetAndOpenConnection();
            _insertCommand = DbHelper.GetInsertCommand(_databaseConnection);
            _status = new FileSetJobLoadingStatus();
            LoadStatuses = new Dictionary<ScanSettings, AbstractLoadingStatus>();

            this.HaveInsertedData = false;
        }

        public void Dispose()
        {
            Logging.Writeline("UlsDataSet - Disposing");
        }

        #endregion

        #region Properties

        /// <summary>
        /// Real-time ULS log monitor
        /// </summary>
        private LogFileWatcher LogfileWatcher
        {
            get { return _logfileWatcher; }
            set { _logfileWatcher = value; }
        }

        /// <summary>
        /// If the LogFileWatcher is running or not
        /// </summary>
        public bool WatcherRunning
        {
            get { return this.LogfileWatcher != null && this.LogfileWatcher.IsWatchingFiles; }
        }

        /// <summary>
        /// Insert command used by dataset
        /// </summary>
        internal SQLiteCommand InsertCommand { get { return this._insertCommand; } }

        /// <summary>
        /// SQLite db created for this process?
        /// </summary>
        internal bool DatabaseCreated { get; set; }

        /// <summary>
        /// Connection to DB
        /// </summary>
        public SQLiteConnection DatabaseConnection { get { return _databaseConnection; } }

        /// <summary>
        /// List of load statuses
        /// </summary>
        public Dictionary<ScanSettings, AbstractLoadingStatus> LoadStatuses { get; set; }

        private object loadStatusLock = new object();

        /// <summary>
        /// Specifies whether there's any data to show yet
        /// </summary>
        public bool HaveInsertedData { get; set; }

        #endregion

        #region Event Definitions

        /// <summary>
        /// Loading status info
        /// </summary>
        public class LoadingStatusEventArgs : EventArgs
        {
            public FileSetJobLoadingStatus Status { get; set; }
            public LoadingStatusEventArgs(FileSetJobLoadingStatus s)
            {
                this.Status = s;
            }
        }

        /// <summary>
        /// New data-load status. Used for file and real-time.
        /// </summary>
        public event EventHandler HaveNewLoadStatus;


        /// <summary>
        /// Raise load status to the GUI for an associated load job (file or real-time)
        /// </summary>
        private void OnHaveNewLoadStatus(ScanSettings settings)
        {
            // Check we have consumers of the event
            if (HaveNewLoadStatus != null)
            {
                AbstractLoadingStatus status = this.LoadStatuses[settings];

                // Call event for MainForm to cach. It will extract the status from the app-core
                HaveNewLoadStatus(this, EventArgs.Empty);

                // Debug
                if (status is FileSetJobLoadingStatus)
                {
                    Logging.Writeline("UlsDataSet.OnHaveMoreData() - Loaded {0:N} of {1:N} bytes - {2}% done.",
                        ((FileSetJobLoadingStatus)status).BytesProcessed,
                        ((FileSetJobLoadingStatus)status).BytesTotal,
                        status.PercentageComplete);
                }
            }
        }

        /// <summary>
        /// Load job has been cancelled
        /// </summary>
        public event EventHandler LoadCancelled;
        private void OnLoadCancelled()
        {
            if (LoadCancelled != null) LoadCancelled(this, EventArgs.Empty);
        }

        /// <summary>
        /// Real-time ULS file watcher has stopped
        /// </summary>
        public event WatcherStoppedDelegate WatcherStopped;
        public delegate void WatcherStoppedDelegate(object sender, EventArgs e);
        private void OnWatcherStopped()
        {
            if (WatcherStopped != null)
            {
                WatcherStopped(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Real-time ULS file watcher has stopped
        /// </summary>
        public event WatcherStartedDelegate WatcherStarted;
        public delegate void WatcherStartedDelegate(object sender, WatcherStartedEventArgs e);
        private void OnWatcherStarted(List<System.IO.DirectoryInfo> dirs)
        {
            if (WatcherStarted != null)
            {
                WatcherStarted(this, new WatcherStartedEventArgs(dirs));
            }
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// A file has loaded a new section
        /// </summary>
        void fileLoader_UpdateFileLoadProgress(object sender, UlsFileReader.FileLoadEventArgs e)
        {
            // Do this so we don't raise HaveMoreData until there's something to read
            if (!this.HaveInsertedData && e.LinesInserted > 0)
            {
                this.HaveInsertedData = true;
            }

            // Update status
            lock (fileStatsLoadingLock)
            {
                //Logging.Writeline("fileLoader_UpdateFileLoadProgress filename={0}, BytesLoaded={1}", e.File.FullName, e.BytesLoaded);
                AbstractLoadingStatus s = this.LoadStatuses[e.Settings];
                if (s is FileSetJobLoadingStatus)
                {
                    FileSetJobLoadingStatus status = (FileSetJobLoadingStatus)s;
                    status.SetFileLoadingProgress(e.File.FullName, e.BytesLoaded);
                }

            }
            OnHaveNewLoadStatus(e.Settings);
        }


        /// <summary>
        /// RT log watcher has found new entries
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void LogfileWatcher_NewLogEntries(object sender, NewEventLogsEventArgs e)
        {
            // Insert new items
            foreach (AbstractUlsLogEntry newEntry in e.NewLogEntries)
            {
                DbHelper.AddLine(newEntry, this.InsertCommand);
            }

            // Allow events to fire
            this.HaveInsertedData = true;

            // Set inserts
            RealTimeLoadingStatus statusAboutToBeRaised = (RealTimeLoadingStatus)this.LoadStatuses[e.Settings];
            statusAboutToBeRaised.RowsInserted = e.NewLogEntries.Count;

            // Refresh
            OnHaveNewLoadStatus(e.Settings);
        }

        #endregion

        // Data load methods
        #region Dataset Methods

        public AbstractLoadingStatus GetLoadStatus(ScanSettings ulsScanSettings)
        {
            return this.LoadStatuses[ulsScanSettings];
        }

        /// <summary>
        /// Load (threaded entrypoint)
        /// </summary>
        public void AppendLogFiles(object settings)
        {
            if (settings is UlsScanSettings)
                AppendLogFiles((UlsScanSettings)settings);
            else
                throw new ArgumentOutOfRangeException("Expected UlsScanSettings");
        }

        static object fileStatsLoadingLock = new object();

        /// <summary>
        /// Loads all ULS files and adds to current dataset
        /// </summary>
        public void AppendLogFiles(UlsScanSettings settings)
        {
            // Environmental
            int filesProcessedCount = 1;

            // Progress
            FileSetJobLoadingStatus thisJobStatus = new FileSetJobLoadingStatus(settings);
            this.LoadStatuses.Add(settings, thisJobStatus);

            // Keep track of file-loaders to unbind events
            List<UlsFileReader> fileLoaders = new List<UlsFileReader>();

            // Process each file
            foreach (System.IO.FileInfo fileToLoad in settings.FilesToBeScanned)
            {
                Logging.Writeline("\n--UlsDataSet - Reading {0} - {1} of {2}...", fileToLoad.Name, filesProcessedCount++, settings.FilesToBeScanned.Count);

                thisJobStatus.CurrentFile = fileToLoad;

                // Begin load for each file
                UlsFileReader fileLoader = new UlsFileReader(this, fileToLoad, settings);
                fileLoader.UpdateFileLoadProgress += fileLoader_UpdateFileLoadProgress;
                fileLoaders.Add(fileLoader);
                fileLoader.LoadFile();

                // Update stats
                lock (fileStatsLoadingLock)
                {
                    thisJobStatus.SetFileLoadingProgress(fileToLoad.FullName, fileToLoad.Length);
                }

                // Exit file-loop if user cancelled
                if (thisJobStatus.UserCancelledLoad) { break; }
            }


            // Wait for threads
            int threadCount = 0;
            lock (FileChunkLoader.threadsListLock)
            {
                threadCount = thisJobStatus.LoaderThreadCount;
            }
            while (threadCount > 0)
            {
                Thread.Sleep(500);
                Logging.Writeline("UlsDataSet - Waiting for {0} parser threads...", threadCount);
                lock (FileChunkLoader.threadsListLock)
                {
                    threadCount = thisJobStatus.LoaderThreadCount;
                }
            }

            // Unbind events
            foreach (UlsFileReader doneFileLoader in fileLoaders)
            {
                doneFileLoader.UpdateFileLoadProgress -= fileLoader_UpdateFileLoadProgress;
            }

            // Cancelled or completed normally?
            if (thisJobStatus.UserCancelledLoad)
            {
                Logging.Writeline("UlsDataSet - Load cancelled after {0} seconds.", DateTime.Now.Subtract(thisJobStatus.LoadStartTime).TotalSeconds);

                // If user cancelled, raise event now to say clean-up is complete
                OnLoadCancelled();
            }
            else
            {
                // All files done, set a new status as "100% done" status
                this.LoadStatuses[settings] = new FileSetLoadCompleteStatus(settings);

                Logging.Writeline("UlsDataSet - All Done in {0} seconds.", DateTime.Now.Subtract(thisJobStatus.LoadStartTime).TotalSeconds);
                OnHaveNewLoadStatus(settings);
            }
        }

        /// <summary>
        /// Clears out results
        /// </summary>
        public void ClearResults()
        {
            DbHelper.ResetData(this.DatabaseConnection);
        }

        #endregion

        // Data query methods
        #region SELECT Methods

        /// <summary>
        /// Does this dataset have an id for a specific query
        /// </summary>
        /// <param name="query">Query used to load data</param>
        /// <param name="id">ID wanted</param>
        /// <returns>If the ID exists in the query</returns>
        public bool ResultHaveId(SimpleSqlSelectStatementInfo query, int id)
        {
            // Check for ID
            MakeSureQueryHasMandatoryFields(query);

            string sql = string.Format("select id from ({0}) where id={1}", query.ToString(), id);
            using (SQLiteDataReader r = DbHelper.LoadItemsFromSQL(sql, this.DatabaseConnection))
            {
                if (r.Read())
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }

        /// <summary>
        /// Returns the id position of a specific record ID
        /// </summary>
        /// <param name="query"></param>
        /// <param name="id"></param>
        /// <returns></returns>
        public int GetIdIndex(SimpleSqlSelectStatementInfo query, int id)
        {
            // Check for ID
            MakeSureQueryHasMandatoryFields(query);
            int idx = -1;

            string sql = string.Format("select id from ({0})", query.ToString(), id);
            using (SQLiteDataReader r = DbHelper.LoadItemsFromSQL(sql, this.DatabaseConnection))
            {
                bool continueReading = r.Read();
                while (continueReading)
                {
                    idx++;

                    // Keep reading while the id read isn't the one we want && there's still more to read.
                    continueReading = (r.GetInt32(0) != id) && r.Read();
                }
            }

            return idx;
        }

        /// <summary>
        /// Gets the latest date for ULS records loaded. Returns DateTime.Min if none.
        /// </summary>
        /// <returns></returns>
        public DateTime GetLastRecordDate()
        {
            string sql = string.Format("select max({0}) from {1}", Constants.FIELD_NAME_TIMESTAMP, Constants.TABLE_NAME_LOGDATA);
            System.Data.IDataReader r = DbHelper.LoadItemsFromSQL(sql, this.DatabaseConnection);

            if (r.Read())
            {
                if (!r.IsDBNull(0))
                {
                    DateTime d = r.GetDateTime(0);
                    return d;
                }
                else
                {
                    return DateTime.MinValue;
                }
            }
            else
            {
                return DateTime.MinValue;
            }
        }

        /// <summary>
        /// Get number of items processed for query
        /// </summary>
        public int GetItemCount(SimpleSqlSelectStatementInfo queryWithFilter)
        {
            return DbHelper.GetItemCount(this.DatabaseConnection, queryWithFilter);
        }

        /// <summary>
        /// Return a record-count for all entries
        /// </summary>
        /// <returns></returns>
        public int GetItemCount()
        {
            return this.GetItemCount(SimpleSqlSelectStatementInfo.BasicIdOnlyQuery);
        }

        /// <summary>
        /// Load new stats info for the dataset
        /// </summary>
        /// <param name="currentViewQuery"></param>
        /// <returns></returns>
        public FullDataStatistics GetRecordStats(SimpleSqlSelectStatementInfo currentViewQuery)
        {
            if (!currentViewQuery.StatmentIsValid) throw new InvalidOperationException("Query is invalid");


            Logging.Writeline("UlsDataSet.UpdateStats - Refreshing stats...");
            // Get stats
            int totalFilterRecords = this.GetItemCount(currentViewQuery);
            int totalRecordsInserted = this.GetItemCount();

            // Calculate level stats
            string levelSql = "select id from " + Constants.TABLE_NAME_LOGDATA + " where " + Constants.FIELD_NAME_LEVEL + " = '{0}'";
            int totalVerbose = this.GetItemCount(new SimpleSqlSelectStatementInfo(string.Format(levelSql, Constants.LEVEL_VERBOSE)));
            int totalInfo = this.GetItemCount(new SimpleSqlSelectStatementInfo(string.Format(levelSql, Constants.LEVEL_INF)));
            int totalWarning = this.GetItemCount(new SimpleSqlSelectStatementInfo(string.Format(levelSql, Constants.LEVEL_WANRING)));
            int totalHigh = this.GetItemCount(new SimpleSqlSelectStatementInfo(string.Format(levelSql, Constants.LEVEL_HIGH)));
            int totalCritical = this.GetItemCount(new SimpleSqlSelectStatementInfo(string.Format(levelSql, Constants.LEVEL_CRITICAL)));
            int totalUnexpected = this.GetItemCount(new SimpleSqlSelectStatementInfo(string.Format(levelSql, Constants.LEVEL_UNEXPECTED)));
            int totalExceptions = this.GetItemCount(new SimpleSqlSelectStatementInfo(string.Format(levelSql, Constants.LEVEL_EXCEPTION)));

            // Return data
            return new FullDataStatistics(totalVerbose, totalInfo, totalWarning, totalHigh, totalCritical, totalUnexpected, totalExceptions, totalRecordsInserted, totalFilterRecords);
        }

        /// <summary>
        /// Get log-items from DB
        /// </summary>
        public List<DatabaseLoadedUlsLogEntry> ExecuteQuery(SimpleSqlSelectStatementInfo sql)
        {
            if (sql == null) throw new ArgumentNullException("sql");
            if (!sql.StatmentIsValid) throw new ArgumentOutOfRangeException("SQL isn't valid");

            // Always have an ID field
            if (!sql.FieldCollection.Contains(Constants.FIELD_NAME_ID))
                sql.FieldCollection.Add(Constants.FIELD_NAME_ID);

            List<DatabaseLoadedUlsLogEntry> items = new List<DatabaseLoadedUlsLogEntry>();

            // Get record(s) from DB
            SQLiteDataReader r = DbHelper.LoadItemsFromSQL(sql.ToString(), this.DatabaseConnection);

            DataReaderCache readerCache = new DataReaderCache(r);
            while (r.Read())
            {
                // Load log entry from DB
                DatabaseLoadedUlsLogEntry u = DatabaseLoadedUlsLogEntry.LoadFromReader(readerCache);
                items.Add(u);
            }

            return items;
        }

        public List<DatabaseLoadedUlsLogEntry> GetRangeSelectItems(int fromRowId, int toRowId, SimpleSqlSelectStatementInfo baseQuery)
        {

            // Check for minimum required fields
            MakeSureQueryHasMandatoryFields(baseQuery);

            RowLimitSqlSelectStatement rowLimitQuery = new RowLimitSqlSelectStatement(baseQuery, fromRowId, toRowId);
            MakeSureQueryHasMandatoryFields(rowLimitQuery);

            List<DatabaseLoadedUlsLogEntry> wantedItems = this.ExecuteQuery(rowLimitQuery);
            return wantedItems;
        }

        private void MakeSureQueryHasMandatoryFields(SimpleSqlSelectStatementInfo query)
        {
            //if (!query.FieldCollection.Contains(Constants.FIELD_NAME_ROWID))
            //    query.FieldCollection.Add(Constants.FIELD_NAME_ROWID);
            if (!query.FieldCollection.Contains(Constants.FIELD_NAME_ID))
                query.FieldCollection.Add(Constants.FIELD_NAME_ID);
        }

        #endregion

        #region File Monitoring Methods

        private RealTimeScanSettings lastFileWatcherSettings = null;

        public void StopLogMonitoring()
        {
            if (this.LogfileWatcher != null)
            {
                this.LogfileWatcher.ContinueWatching = false;
            }
        }


        /// <summary>
        /// Start monitoring specific directories for changes from either the last entry or now if no last entry
        /// </summary>
        public void StartWatching(RealTimeScanSettings fileWatcherSettings)
        {
            // Add status
            this.LoadStatuses.Add(fileWatcherSettings, new RealTimeLoadingStatus(fileWatcherSettings));
            lastFileWatcherSettings = fileWatcherSettings;

            // Instantiate watcher
            if (this.LogfileWatcher == null)
            {
                this.LogfileWatcher = new LogFileWatcher(fileWatcherSettings);
                this.LogfileWatcher.NewLogEntries += LogfileWatcher_NewLogEntries;
                this.LogfileWatcher.WatcherStopped += LogfileWatcher_WatcherStopped;
                this.LogfileWatcher.WatcherStarted += LogfileWatcher_WatcherStarted;
            }

            // Figure out when last known ULS record was
            DateTime lastEntryDate = this.GetLastRecordDate();
            if (lastEntryDate == DateTime.MinValue)
            {
                lastEntryDate = DateTime.Now;
            }

            // Reset watcher settings
            this.LogfileWatcher.DirectoriesToWatch.Clear();
            this.LogfileWatcher.DirectoriesToWatch.AddRange(fileWatcherSettings.Directories);
            this.LogfileWatcher.BeginMonitoring(lastEntryDate);

        }

        void LogfileWatcher_WatcherStarted(object sender, WatcherStartedEventArgs e)
        {
            OnWatcherStarted(e.DirectoryTargets);
        }

        void LogfileWatcher_WatcherStopped(object sender, EventArgs e)
        {
            // Remove settings
            this.LoadStatuses.Remove(lastFileWatcherSettings);


            OnWatcherStopped();
        }

        #endregion
    }
}
